﻿using JavaVirtualMachine.ConstantPoolItems;
using JavaVirtualMachine.StackTracePrinters;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.IO.Compression;
using System.Linq;

namespace JavaVirtualMachine
{
    public struct Config
    {
        public string rtPath { get; set; }
        public string javaHome { get; set; }
        public string[] androidPath { get; set; }
        public string[] srcPaths { get; set; }
        public PrintStackTraceOptions printStackTrace { get; set; }

        public string GetSrcPath()
        {
            return srcPaths[0].Replace(".jar", "");
        }
    }

    public class Program
    {
        public static Stopwatch Stopwatch = new Stopwatch();
        public static Config Configuration;
        public static IStackTracePrinter StackTracePrinter = EmptyStackTracePrinter.Instance;
        public static string AppName;
        public static int Index;
        public static bool ThreadInit;
        public static List< Thread> threads=new List<Thread>();
        public static List<Memory<int>> GlobalStacks = new List<Memory<int>>();
        public static readonly object _locker = new object();
        public static void Main(string[] args)
        {
            //System.Text.Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
            if (args.Length != 0)
            {
                //Get configuration
                //string configPath = @"..\..\..\config.json";
                //string configPath = args[0];

                //Configuration = JsonConvert.DeserializeObject<Config>(File.ReadAllText(configPath));

                //switch(Configuration.printStackTrace)
                //{
                //    case PrintStackTraceOptions.Window:
                //        //Start stack trace console by running current process with different args
                //        string currProcessName = Environment.CommandLine;
                //        currProcessName = currProcessName.Trim('"', ' ');
                //        currProcessName = Path.ChangeExtension(currProcessName, ".exe");
                //        if (currProcessName.Contains(Environment.CurrentDirectory))
                //        {
                //            currProcessName = currProcessName.Replace(Environment.CurrentDirectory, string.Empty);
                //        }
                //        currProcessName = currProcessName.Replace("\\", "");
                //        currProcessName = currProcessName.Replace("\"", "");
                //        ProcessStartInfo stcStartInfo = new ProcessStartInfo(currProcessName, "stacktrace " + Process.GetCurrentProcess().Id);
                //        stcStartInfo.UseShellExecute = true;
                //        Process.Start(stcStartInfo);

                //        //Open pipe to communicate with printer process
                //        NamedPipeServerStream serverStream = new("JVMStackTrace", PipeDirection.Out, 1);
                //        ColoredStream coloredStream = new(serverStream);
                //        StackTracePrinter = new ColorfulStackTracePrinter((color, str) => coloredStream.WriteString(color, str));
                //        serverStream.WaitForConnection();
                //        break;
                //    case PrintStackTraceOptions.File:
                //        string stOutputFile = @"..\..\..\stackTrace.txt";
                //        if(File.Exists(stOutputFile))
                //        {
                //            File.Delete(stOutputFile);
                //        }
                //        FileStream fileStream = File.OpenWrite(stOutputFile);
                //        StreamWriter fileWriter = new (fileStream);
                //        StackTracePrinter = new ColorfulStackTracePrinter((color, str) => 
                //        {
                //            fileWriter.Write(str); fileWriter.Flush(); 
                //        });
                //        break;
                //}

                JVM(args);
            }
            else if (args[0] == "stacktrace")
            {
                StackTraceConsole(parentID: int.Parse(args[1]));
            }
        }

        static void StackTraceConsole(int parentID)
        {
            Process parentProcess = Process.GetProcessById(parentID);
            using NamedPipeClientStream clientStream = new(".", "JVMStackTrace", PipeDirection.In, PipeOptions.None);
            clientStream.Connect();

            ColoredStream colorStream = new(clientStream);
            while (!parentProcess.HasExited)
            {
                (byte color, string str) = colorStream.ReadString();
                Console.ForegroundColor = (ConsoleColor)color;
                Console.Write(str);
            }
        }

        static void InitializeSystem()
        {
            ClassFile systemCFile = ClassFileManager.GetClassFile("java/lang/System");
            MethodInfo initSystemClassMethod = systemCFile.MethodDictionary[("initializeSystemClass", "()V")];

            Executor.BeginExecution(initSystemClassMethod);
        }

        static void InitializeAndriod()
        {
            int args = 0;

            DisplayManagerService = AddSersvice("base.service.DisplayManagerService", "display");
            DropBoxManagerService = AddSersvice("base.service.DropBoxManagerService", "dropbox");
            WindowManagerService = AddSersvice("base.service.DropBoxManagerService", "window");

            //LooperClass = ExecutionStaticMethod("android.os.Looper", "prepare", null, args);

            //ActivityThreadClass = ExecutionStaticMethod("android.app.ActivityThread", "main", null, args);

            LooperClass = ExecutionStaticMethod("android.os.Looper", "prepareMainLooper", null, args);
            ActivityThreadClass = ExecutionStaticMethod("android.app.ActivityThread", "systemMain");
            ActivityThreadClass.Item2 = JavaHelper.CurrentReturnValue;

            ApplicationClass = ExecutionStaticMethod("android.app.ActivityThread", "currentApplication");
            ApplicationClass.Item2 = JavaHelper.CurrentReturnValue;


            ExecutionMethod(ActivityThreadClass.Item1, "getApplicationThread",null, ActivityThreadClass.Item2);
            ApplicationThreadClass.Item2 = JavaHelper.CurrentReturnValue;
        }

        static (ClassFile,int) AddSersvice(string className,string key)
        {
            (ClassFile, int) Service = Execution(className);
            int arg = JavaHelper.CreateJavaStringLiteral(key);
            ExecutionStaticMethod("android.os.ServiceManager", "addService", null, arg, Service.Item2);
            return Service;
        }

        public static JarFile JarFile;

        public static string GetMIDletName()
        {
            return Program.JarFile.Manifest.MIDlets[0][0];
        }
        static int MIDletIndex;
        public static string GetManifest(string info)
        {
            try
            {
                switch (info)
                {
                    case "MIDlet-Name":
                        return JarFile.Manifest.MIDletName;
                    case "MIDlet-Version":
                        return JarFile.Manifest.MIDletVersion;
                    case "MIDlet-Icon":
                        return JarFile.Manifest.MIDletIcon;
                    default:
                        
                        break;
                }

                MIDletIndex = int.Parse(info);
            }
            catch
            {
                UnityEngine.Debug.LogWarning("GetManifestData：" + info);
                info = info.ToLower();
                if (JarFile.Manifest.Datas.ContainsKey(info))
                {
                    string ManifestData = JarFile.Manifest.Datas[info];
                    UnityEngine.Debug.LogWarning("ManifestData：" + ManifestData);
                    return ManifestData;
                }
                UnityEngine.Debug.LogWarning("ManifestData:Null");
                return null;
            }
            MIDletIndex +=1;
            if (MIDletIndex >= JarFile.Manifest.MIDlets.Count)
                return null;

            string str = "";
            foreach (var item in JarFile.Manifest.MIDlets[MIDletIndex])
            {
                str += "," + item;
            }

            return str.Substring(1);
        }

        private static JarFile OpenJarAsync(string path)
        {
            if (string.IsNullOrWhiteSpace(path) || !File.Exists(path) || !".jar".Equals(Path.GetExtension(path), StringComparison.OrdinalIgnoreCase))
            {
                UnityEngine. Debug.LogWarning("Cannot open .jar from path = '{"+path+"}'");
                return null;
            }

            try
            {
                JarFile JarFile = new JarFile(path);

                // read manifest data
                using var stream = JarFile.GetReStream("META-INF/MANIFEST.MF");
                if (stream != null)
                {
                    using var reader = new StreamReader(stream);
                    JarFile.ParseManifestAsync(reader);
                }
                return JarFile;
            }
            catch (Exception ex)
            {
                UnityEngine.Debug.LogError("Failed to open jar file from path = '{" + path + "}'");
                return null;
            }
        }
        static void JVM(string[] args)
        {
            Stopwatch.Start();
            AppName = Configuration.srcPaths[0];
            JarFile= OpenJarAsync(AppName);
            //%JAVA_HOME%\bin\javap" - s -p -c -verbose Scanner.class > ..\..\..\Scanner.javap
            //ClassFileManager.InitDictionary(runtimePath: Configuration.rtPath, otherPaths: Configuration.srcPaths);
            ClassFileManager.InitDictionary(Configuration.rtPath,AppName,JarFile.ClassFiles, Configuration.androidPath);

            //Create main thread object
            int threadGroupAddr = Heap.CreateObject(ClassFileManager.GetClassFileIndex("java/lang/ThreadGroup"));

            ThreadManager.ThreadAddr = Heap.CreateObject(ClassFileManager.GetClassFileIndex("java/lang/Thread"));
            HeapObject threadObj = Heap.GetObject(ThreadManager.ThreadAddr);

            threadObj.SetField("group", "Ljava/lang/ThreadGroup;", threadGroupAddr);
            threadObj.SetField("priority", "I", 5);
            InitializeSystem();
            ThreadInit = true;

            if (Executor.ActiveException != 0)
            {
                // Todo: Print exception type, message, stack trace
                DebugWriter.WriteDebugMessage("Program terminated with an exception");
                return;
            }

            if(JarFile.Manifest.MIDlets.Count>0)
            {
                StartClass = Execution(JarFile.Manifest.MIDlets[0][2]);
                StartApp("startApp");
            }else
            {
                InitializeAndriod();
                int code = 7;
                //ExecutionMethod(ApplicationThreadClass.Item1, "onTransact", null, ApplicationThreadClass.Item2);

                StartClass = Execution("com.june.game.doudizhu.activities.game.StartActivity");
                HeapObject StartActivity = Heap.GetObject(StartClass.Item2);

                ActivityInfoClass = Execution("android.content.pm.ActivityInfo");
                StartActivity.SetField("mActivityInfo", "Landroid/content/pm/ActivityInfo;", ActivityInfoClass.Item2);
                //ExecutionStaticMethod("android.app.ActivityThread", "getInstrumentation");
                //int Instrumentation = JavaHelper.CurrentReturnValue;

                StartActivity.SetField("mApplication", "Landroid/app/Application;", ApplicationClass.Item2);

                ExecutionMethod(StartClass.Item1, "attachBaseContext", null, StartClass.Item2, ApplicationClass.Item2);

                WindowsClass = Execution("base.Windows", StartClass.Item2);
                HeapObject windons = Heap.GetObject(WindowsClass.Item2);
                StartActivity.SetField("mWindow", "Landroid/view/Window;", WindowsClass.Item2);
                ExecutionMethod(WindowsClass.Item1, "getWindowManager", null, WindowsClass.Item2);
                WindowManagerClass.Item2 = JavaHelper.CurrentReturnValue;
                StartActivity.SetField("mWindowManager", "Landroid/view/WindowManager;", WindowManagerClass.Item2);

                ExecutionMethod(StartClass.Item1, "getFragmentManager", null, StartClass.Item2);
                int FragmentManagerAddr = JavaHelper.CurrentReturnValue;
                HeapObject Fragment= Heap.GetObject(FragmentManagerAddr);
                ExecutionMethod(Fragment.ClassFile, "attachActivity", null, FragmentManagerAddr, StartClass.Item2, StartActivity.GetField("mContainer", "Landroid/app/FragmentContainer;"),0);
                int Bundle = 0;
                ExecutionMethod(StartClass.Item1, "onCreate", null, StartClass.Item2, Bundle);
                //DebugWriter.WriteDebugMessage("Not find MIDlets");
            }
           
        }

        public static (ClassFile, int) StartClass;
        public static (ClassFile, int) ActivityInfoClass;
        public static (ClassFile, int) WindowsClass;
        public static (ClassFile, int) WindowManagerClass;
        public static (ClassFile, int) ApplicationClass;
        public static (ClassFile, int) ApplicationThreadClass;
        public static (ClassFile, int) LooperClass;
        public static (ClassFile, int) DisplayManagerService;
        public static (ClassFile, int) DropBoxManagerService;
        public static (ClassFile, int) WindowManagerService;
        public static (ClassFile, int) ActivityThreadClass;
        public static bool IsJ2ME;
        public static void StartApp(string name="Main")
        {
            ExecutionMethod(StartClass.Item1, name, null, StartClass.Item2);
        }

        public static void CallKeyPressedMethod(int keyCode, int displayableId)
        {
            HeapObject classObj = Heap.GetObject(displayableId);
            MethodInfo methodInfo = classObj.ClassFile.MethodDictionary[("keyPressed", "(I)V")];
            Executor.BeginExecution(methodInfo, displayableId, keyCode);
        }

        public static void CallKeyReleasedMethod(int keyCode, int displayableId)
        {
            HeapObject classObj = Heap.GetObject(displayableId);
            MethodInfo methodInfo = classObj.ClassFile.MethodDictionary[("keyReleased", "(I)V")];
            Executor.BeginExecution(methodInfo, displayableId, keyCode);
        }

        public static void CallCommandActionMethod(int commandAddr,int displayableId)
        {
            int addr = Heap.GetObject(displayableId).GetField("commandListener", "Ljavax/microedition/lcdui/CommandListener;");
            HeapObject classObj = Heap.GetObject(addr);
            MethodInfo methodInfo = classObj.ClassFile.MethodDictionary[("commandAction", "(Ljavax/microedition/lcdui/Command;Ljavax/microedition/lcdui/Displayable;)V")];
            Executor.BeginExecution(methodInfo, addr, commandAddr, displayableId);
        }

        public static void CallListCommandActionMethod(int addr,int displayableId)
        {
            HeapObject classObj = Heap.GetObject(addr);
            int commandAddr = (int)classObj.ClassFile.StaticFieldsDictionary[("SELECT_COMMAND", "Ljavax/microedition/lcdui/Command;")];
          
            CallCommandActionMethod( commandAddr, displayableId);
        }

        public static void CallListSetSelectedIndexMethod(int addr, int index, bool isBool=false)
        {
            HeapObject classObj = Heap.GetObject(addr);
            MethodInfo methodInfo = classObj.ClassFile.MethodDictionary[("setSelectedIndex", "(IZ)V")];
            Executor.BeginExecution(methodInfo, addr, index, isBool ?1:0);
        }

        public static void SetStaticField(string className, string fieldName = "", long value = 0)
        {
            int mainProgCFileIdx = ClassFileManager.GetClassFileIndex(className);
            ClassFile cFile = ClassFileManager.ClassFiles[mainProgCFileIdx];
            Dictionary<(string name, string descriptor), long> CPInfos = cFile.StaticFieldsDictionary;
            foreach (var item in CPInfos)
            {
                if (item.Key.name == fieldName)
                {
                    switch (item.Key.descriptor[0])
                    {
                        case 'Z':
                        case 'B':
                        case 'C':
                        case 'S':
                        case 'I':
                        case 'F':
                            {
                                cFile.StaticFieldsDictionary[(item.Key.name, item.Key.descriptor)] = (int)value;
                            }
                            break;
                        case 'D':
                        case 'J':
                            {
                                cFile.StaticFieldsDictionary[(item.Key.name, item.Key.descriptor)] = value;
                            }
                            break;
                        case 'L':
                        case '[':
                            {
                                int valueRef = (int)value;
                                cFile.StaticFieldsDictionary[(item.Key.name, item.Key.descriptor)] = valueRef;
                            }
                            break;
                    }
                    break;
                }
            }
        }

        static (ClassFile, int) GetClassFileAndAddr(string className,bool isCreateObject=false)
        {
            int mainProgCFileIdx = ClassFileManager.GetClassFileIndex(className);
            ClassFile mainProg = ClassFileManager.ClassFiles[mainProgCFileIdx];
            int mainProgObjAddr = isCreateObject? Heap.CreateObject(mainProgCFileIdx):0;
            return (mainProg, mainProgObjAddr);
        }

        /// <summary>
        /// 构造类
        /// </summary>
        /// <param name="className">类名</param>
        /// <returns></returns>
        public static (ClassFile,int) Execution(string className, params int[] arguments)
        {
            int mainProgCFileIdx = ClassFileManager.GetClassFileIndex(className);
            ClassFile mainProg = ClassFileManager.ClassFiles[mainProgCFileIdx];
            int mainProgObjAddr = Heap.CreateObject(mainProgCFileIdx);
            int[] args = new int[arguments.Length +1];
            args[0] = mainProgObjAddr;
            if (arguments.Length > 0)
                arguments.CopyTo(args, 1);
            if (mainProg.MethodDictionary.TryGetValue(("<clinit>", "()V"), out MethodInfo clinitMethod))
            {
                Executor.BeginExecution(clinitMethod, args);
                if (Executor.ActiveException != 0)
                {
                    // Todo: Print exception type, message, stack trace
                    DebugWriter.WriteDebugMessage("Program terminated with an exception");
                    throw new InvalidOperationException();
                }
            }
            MethodInfo mainProgInit = null;
            foreach (var item in mainProg.MethodDictionary)
            {
                if(item.Key .name == "<init>")
                {
                    mainProgInit = item.Value;
                    break;
                }
            }
            
            //MethodInfo mainProgInit = mainProg.MethodDictionary[("<init>", "()V")];

            Executor.BeginExecution(mainProgInit, args);
            if (Executor.ActiveException != 0)
            {
                // Todo: Print exception type, message, stack trace
                HeapObject heapObject = Heap.GetObject(Executor.ActiveException);
                ClassFile exceptionClassFile = heapObject.ClassFile;
                string message=JavaHelper. ReadJavaString(heapObject.GetField("detailMessage", "Ljava/lang/String;"));
                DebugWriter.WriteDebugMessage("Program terminated with an exception: "+ exceptionClassFile.Name+ "  message:" + message);
                throw new InvalidOperationException();
            }
            //Execution("emulator/Emulator");

            return (mainProg, mainProgObjAddr);
        }

        static (ClassFile, int) ExecutionStaticMethod(string className, string methodName = "main", string descriptor = null, params int[] arguments)
        {
            int mainProgCFileIdx = ClassFileManager.GetClassFileIndex(className);
            ClassFile mainProg = ClassFileManager.ClassFiles[mainProgCFileIdx];

            ExecutionMethod(mainProg, methodName, descriptor, arguments);

            return (mainProg, 0);
        }

        static void ExecutionMethod(ClassFile mainProg, string methodName = "main",string descriptor = null, params int[] arguments)
        {
            foreach (var method in mainProg.MethodDictionary)
            {
                if (method.Key.name == methodName)
                {
                    if(descriptor!=null)
                    {
                        if (method.Key.descriptor != descriptor)
                        {
                            continue;
                        }
                    }
                    Executor.BeginExecution(method.Value, arguments);
                    if (Executor.ActiveException != 0)
                    {
                        // Todo: Print exception type, message, stack trace
                        DebugWriter.WriteDebugMessage("Program terminated with an exception");
                        throw new InvalidOperationException();
                    }
                    return;
                }
            }
            if(mainProg.SuperClass==null)
            {
                return;
            }
            mainProg = mainProg.SuperClass;
            ExecutionMethod(mainProg, methodName, descriptor, arguments);
        }
    }
}
